﻿using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI.Xaml.Media.Imaging;

namespace TopoViewer
{
    [StructLayout(LayoutKind.Explicit)]
    struct ImageMatrixHeader
    {
        [FieldOffset(0x00), MarshalAs(UnmanagedType.ByValArray, SizeConst = 4, ArraySubType = UnmanagedType.I1)]
        public char[] extension;       // .jpg or .png
        [FieldOffset(0x04), MarshalAs(UnmanagedType.U4)]
        public uint bitsPerPixel;		// # of bits used per pixel (usually 24 for jpg and 8 for png)
        [FieldOffset(0x08), MarshalAs(UnmanagedType.U4)]
        public uint pixelsPerEntity;	// # of pixels making up an entity. It's either 1 or 2
        [FieldOffset(0x0c), MarshalAs(UnmanagedType.U4)]
        public uint columns;			// # of columns in this image matrix
        [FieldOffset(0x10), MarshalAs(UnmanagedType.U4)]
        public uint rows;				// # of rows in this image matrix
        [FieldOffset(0x14), MarshalAs(UnmanagedType.U4)]
        // The following dimensions are for full map, with the matrix of images stitched together, unless flags==256, 
        // But they are for individual images for the elevation & shade images
        public uint width;				// Width of the map in pixels
        [FieldOffset(0x18), MarshalAs(UnmanagedType.U4)]
        public uint height;			// Height of the map in pixels
        [FieldOffset(0x1c), MarshalAs(UnmanagedType.U4)]
        public uint flags;				// Haven't figured out. Only know 256 is a special #, see comments above.
    };

    [StructLayout(LayoutKind.Explicit, Size = 0x0400)]
    struct Header
    {
        [FieldOffset(0x0000), MarshalAs(UnmanagedType.I4)]
        public uint magicNumber;		    // Always 0x00000001
        [FieldOffset(0x0004), MarshalAs(UnmanagedType.R8)]
        public double west;				// West longitude, in degrees
        [FieldOffset(0x000c), MarshalAs(UnmanagedType.R8)]
        public double north;				// North latitude, in degrees
        [FieldOffset(0x0014), MarshalAs(UnmanagedType.R8)]
        public double east;				// East longitude, in degrees
        [FieldOffset(0x001c), MarshalAs(UnmanagedType.R8)]
        public double south;				// South latitude, in degrees
        [FieldOffset(0x0024), MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0xdc)]
        public string seriesName;	        // Always "TOPO!"
        [FieldOffset(0x0100), MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x80)]
        public string quadName;
        [FieldOffset(0x0180), MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
        public string areaName;
        [FieldOffset(0x01a0), MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
        public string source;			    // E.g. "USGS"
        [FieldOffset(0x01c0), MarshalAs(UnmanagedType.ByValArray, SizeConst = 4, ArraySubType = UnmanagedType.I1)]
        public char[] yearPrinted;		    // Year when the map was printed
        [FieldOffset(0x01c4), MarshalAs(UnmanagedType.ByValArray, SizeConst = 4, ArraySubType = UnmanagedType.I1)]
        public char[] yearCollected;	    // Year when the data was collected
        [FieldOffset(0x01c8), MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x08)]
        public string contourInterval;
        [FieldOffset(0x01e0)]
        public ImageMatrixHeader mapImageInfo;
        [FieldOffset(0x0200), MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x50)]
        public string elevationDataDesc;
        [FieldOffset(0x0250), MarshalAs(UnmanagedType.U4)]
        public uint minElevation;		    // min and max elevation, in meters, plus 0x8000 to avoid negatives
        [FieldOffset(0x0254), MarshalAs(UnmanagedType.U4)]
        public uint maxElevation;
        [FieldOffset(0x0258)]
        public ImageMatrixHeader elevationImageInfo;
        [FieldOffset(0x0294)]
        public ImageMatrixHeader shadeImageInfo;
    };

    class PackedStream : IRandomAccessStream
    {
        Stream internalStream;

        public PackedStream(byte[] bytes)
        {
            internalStream = new MemoryStream(bytes);
        }

        public bool CanRead
        {
            get { return true; }
        }

        public bool CanWrite
        {
            get { return false; }
        }

        public IRandomAccessStream CloneStream()
        {
            throw new NotImplementedException();
        }

        public IInputStream GetInputStreamAt(ulong position)
        {
            throw new NotImplementedException();
        }

        public IOutputStream GetOutputStreamAt(ulong position)
        {
            throw new NotImplementedException();
        }

        public ulong Position
        {
            get { return (ulong)internalStream.Position; }
        }

        public void Seek(ulong position)
        {
            internalStream.Seek((long)position, SeekOrigin.Begin);
        }

        public ulong Size
        {
            get
            {
                return (ulong)internalStream.Length;
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public void Dispose()
        {
            throw new NotImplementedException();
        }

        public IAsyncOperationWithProgress<IBuffer, uint> ReadAsync(IBuffer buffer, uint count, InputStreamOptions options)
        {
            return internalStream.AsInputStream().ReadAsync(buffer, count, options);
        }

        public IAsyncOperation<bool> FlushAsync()
        {
            throw new NotImplementedException();
        }

        public IAsyncOperationWithProgress<uint, uint> WriteAsync(IBuffer buffer)
        {
            throw new NotImplementedException();
        }
    }

    public class TpqMap
    {
        private Header fileHeader;
        private uint[] mapImageOffsets;
        private uint[] elevationImageOffsets;
        private uint[] shadeImageOffsets;
        private WriteableBitmap[,] mapletBitmaps;
        private Size<double> mapletSizeWorld;           // Size of each maplet in world coordinate, in seconds.
        private Size<uint> mapletSizePixel;	            // Size of each maplet in local coordinate, in pixels.

        public Tuple<double, double> pixelPerSecond;    // Scale of the map

        public async Task LoadFile(string filename)
        {
            #region Read header from file 
            StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(new Uri(filename));
            IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.Read);

            byte[] headerBuffer = new byte[Marshal.SizeOf<Header>()];
            await stream.ReadAsync(headerBuffer.AsBuffer(), (uint)headerBuffer.Length, InputStreamOptions.None);

            // Make sure that the Garbage Collector doesn't move our buffer 
            GCHandle handle = GCHandle.Alloc(headerBuffer, GCHandleType.Pinned);
            // Marshal the bytes
            fileHeader = Marshal.PtrToStructure<Header>(handle.AddrOfPinnedObject());
            // Give control of the buffer back to the GC 
            handle.Free();
            #endregion

            mapletSizeWorld.width = (fileHeader.east - fileHeader.west) * 3600 / fileHeader.mapImageInfo.columns;
            mapletSizeWorld.height = (fileHeader.north - fileHeader.south) * 3600 / fileHeader.mapImageInfo.rows;

            if (fileHeader.mapImageInfo.flags == 256)
            {
                mapletSizePixel.width = fileHeader.mapImageInfo.width;
                mapletSizePixel.height = fileHeader.mapImageInfo.height;
            }
            else
            {
                mapletSizePixel.width = fileHeader.mapImageInfo.width / fileHeader.mapImageInfo.columns;
                mapletSizePixel.height = fileHeader.mapImageInfo.height / fileHeader.mapImageInfo.rows;
            }

            pixelPerSecond = Tuple.Create(mapletSizePixel.width / mapletSizeWorld.width, -mapletSizePixel.height / mapletSizeWorld.height);	// Y-scale is always negative, since screen coordinate grows downwards.

            DataReader dr = new DataReader(stream.GetInputStreamAt((ulong)headerBuffer.Length));
            dr.ByteOrder = ByteOrder.LittleEndian;

            // Read offsets of map images, elevation images, and shade images.
            // The number of offsets is one more than the number of images. The last one is pointing the end of the file
            // and can be used to determine the size of the last image.
            uint mapImages = fileHeader.mapImageInfo.rows * fileHeader.mapImageInfo.columns;
            mapImageOffsets = new uint[mapImages + 1]; // For all offset arrays, we allocate one more element so that we can determine the size of each image easily.

            uint elevationImages = 2 * fileHeader.elevationImageInfo.rows * fileHeader.elevationImageInfo.columns;
            elevationImageOffsets = new uint[elevationImages + 1];

            uint shadeImages = fileHeader.shadeImageInfo.rows * fileHeader.shadeImageInfo.columns;
            shadeImageOffsets = new uint[shadeImages + 1];

            await dr.LoadAsync((mapImages + elevationImages + shadeImages + 1) * sizeof(uint));

            for (int i = 0; i < mapImages; i++)
            {
                mapImageOffsets[i] = dr.ReadUInt32();
            }

            for (int i = 0; i < elevationImages; i++)
            {
                elevationImageOffsets[i] = dr.ReadUInt32();
            }

            for (int i = 0; i < shadeImages + 1; i++)
            {
                shadeImageOffsets[i] = dr.ReadUInt32();
            }

            mapImageOffsets[mapImages] = elevationImageOffsets[0];
            elevationImageOffsets[elevationImages] = shadeImageOffsets[0];

            #region Read maplet bitmaps
            DataReader mapletReader = new DataReader(stream);
            mapletBitmaps = new WriteableBitmap[fileHeader.mapImageInfo.rows, fileHeader.mapImageInfo.columns];
            for (uint row = 0; row < fileHeader.mapImageInfo.rows; row++)
                for (uint col = 0; col < fileHeader.mapImageInfo.columns; col++)
                {
                    uint index = row * fileHeader.mapImageInfo.columns + col;
                    uint offset = mapImageOffsets[index];
                    uint length = mapImageOffsets[index + 1] - mapImageOffsets[index];
                    stream.Seek(offset);
                    await mapletReader.LoadAsync(length);
                    byte[] bytes = new byte[length];
                    mapletReader.ReadBytes(bytes);

                    PackedStream bitmapStream = new PackedStream(bytes);
                    WriteableBitmap bitmap = await BitmapFactory.New(1, 1).FromStream(bitmapStream);
                    mapletBitmaps[row, col] = bitmap;
                }
            #endregion

            //WriteableBitmap result = BitmapFactory.New((int)width, (int)height);
            //for (uint row = 0; row < fileHeader.mapImageInfo.rows; row++)
            //    for (uint col = 0; col < fileHeader.mapImageInfo.columns; col++)
            //    {
            //        double mapletWidth = fileHeader.mapImageInfo.width / fileHeader.mapImageInfo.columns;
            //        //height / this.fileHeader.mapImageInfo.rows;
            //        double mapletHeight = fileHeader.mapImageInfo.height / fileHeader.mapImageInfo.rows;
            //        //width / this.fileHeader.mapImageInfo.columns;

            //        Rect source = new Rect(0, 0, fileHeader.mapImageInfo.width / fileHeader.mapImageInfo.columns, fileHeader.mapImageInfo.height / fileHeader.mapImageInfo.rows);
            //        Rect dest = new Rect(col * mapletWidth, row * mapletHeight, mapletWidth, mapletHeight);

            //        result.Blit(dest, nativeMapletBitmaps[row, col], source);
            //    }
        }

        public void Draw(WriteableBitmap bitmap, GRect rectWorld, Transformation worldToScreen, bool loadCacheOnly)
        {
            int minCol = (int)Math.Floor((rectWorld.west - fileHeader.west * 3600) / mapletSizeWorld.width);
            int maxCol = (int)Math.Ceiling((rectWorld.east - fileHeader.west * 3600) / mapletSizeWorld.width) - 1;
            int minRow = (int)Math.Floor((fileHeader.north * 3600 - rectWorld.north) / mapletSizeWorld.height);
            int maxRow = (int)Math.Ceiling((fileHeader.north * 3600 - rectWorld.south) / mapletSizeWorld.height) - 1;

            for (int row = minRow; row <= maxRow; row++)
                for (int col = minCol; col <= maxCol; col++)
                {
                    // Current block boundary, in world coordinate system.
                    GRect blockBoundary;
                    blockBoundary.north = fileHeader.north * 3600 - mapletSizeWorld.height * row;
                    blockBoundary.south = fileHeader.north * 3600 - mapletSizeWorld.height * (row + 1);
                    blockBoundary.west = fileHeader.west * 3600 + mapletSizeWorld.width * col;
                    blockBoundary.east = fileHeader.west * 3600 + mapletSizeWorld.width * (col + 1);

                    // Drawing area within this block, in world coordinate system.
                    GRect intersect = rectWorld & blockBoundary;

                    // Drawing area within this block, in screen coordiante system.
                    SRect screen = worldToScreen.Transform(intersect);

                    // World to local coordiante (within the block) transformation.
                    Transformation worldToMaplet;
                    worldToMaplet.xScale = pixelPerSecond.Item1;
                    worldToMaplet.yScale = pixelPerSecond.Item2;
                    worldToMaplet.xDelta = -worldToMaplet.xScale * blockBoundary.west;
                    worldToMaplet.yDelta = -worldToMaplet.yScale * blockBoundary.north;

                    // Rendering area in this grid, in maplet coordinate system.
                    SRect rectMaplet = worldToMaplet.Transform(intersect);
                    try
                    {
                        bitmap.Blit(screen.ToRect(), mapletBitmaps[row, col], rectMaplet.ToRect());
                    }
                    catch (Exception ex)
                    {
                        string s = ex.Message;
                    }
                }
        }
    }
}
